package com.googlecode.gwtbb.client.shared.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

/**
 * @author Abraham Grief
 * @version $Id$
 * @since Apr 30, 2008
 */
public class StringUtils {
	public static final char EXT_DELIM = '.';
	public static final char URL_SEPARATOR_CHAR = '/';
	public static final String URL_SEPARATOR = "" + URL_SEPARATOR_CHAR;

	/**
	 * Performs the inverse of parseCsvLine.
	 * However, passing in null will return null, whereas parseCsvLine will never return null.
	 * Furthermore, null items in fields will be considered as empty strings, even though parseCsvLine will never
	 * return null items.
	 *
	 * @param fields a List of fields to be composed into a single csv string
	 * @return the List of fields in csv form
	 */
	public static String composeCsvLine(List<String> fields) {
		if (fields == null)
			return null;
		StringBuffer buf = new StringBuffer();
		boolean addComma = false;

		for (String field : fields) {
			if (addComma)
				buf.append(',');
			else
				addComma = true;

			if (field != null) {
				boolean wrapQuotes = false;
				if (field.contains("\"")) {
					wrapQuotes = true;
					field = field.replace("\"", "\"\"");
				} else if (field.contains(",")) {
					wrapQuotes = true;
				}
				if (wrapQuotes) {
					buf.append('"');
					buf.append(field);
					buf.append('"');
				} else {
					buf.append(field);
				}
			}
		}
		return buf.toString();
	}

	/**
	 * Parses a line from a csv file into a List of fields.
	 * There will be one String in the List for each field.
	 * Empty fields will appear as empty Strings in the list.
	 * Returns an empty List if the line is null.
	 * <p/>
	 * If the following line appears as so in a text editor:
	 * a,"b","""c""","d,e","f"",""g",
	 * Then the resulting List of Strings will be:
	 * "a", "b", "\"c\"", "d,e", "f\",\"g", ""
	 *
	 * @param line the line from a csv file to be parsed
	 * @return a List of Strings containing the fields from the line
	 * @throws IllegalArgumentException if there is an unclosed quote in the line
	 */
	public static List<String> parseCsvLine(String line) throws IllegalArgumentException {
		List<String> retVal = new ArrayList<String>();
		if (line == null)
			return retVal;
		int numChars = line.length();
		int start = 0;
		int quoteCount = 0;
		int i;
		for (i = 0; i < numChars; i++) {
			char c = line.charAt(i);
			if (c == '"') {
				quoteCount++;
			} else if (c == ',') {
				// If quoteCount is odd, then the comma is within quotes and should be treated as a regular character.
				// If quoteCount is even, then the comma is a delimiter.
				if (quoteCount % 2 == 0) {
					// No quotes, simple value.
					if (quoteCount == 0)
						retVal.add(line.substring(start, i));
					else {
						// Wrapping quotes that need to be stripped.
						String val = line.substring(start + 1, i - 1);
						// Internal double quotes that need to be single.
						if (quoteCount >= 4)
							val = val.replace("\"\"", "\"");
						retVal.add(val);
						quoteCount = 0;
					}
					start = i + 1;
				}
			}
		}
		// The end of the line is the final delimiter.
		if (quoteCount % 2 == 0) {
			// No quotes, simple value.
			if (quoteCount == 0)
				retVal.add(line.substring(start, i));
			else {
				// Wrapping quotes that need to be stripped.
				String val = line.substring(start + 1, i - 1);
				// Internal double quotes that need to be single.
				if (quoteCount >= 4)
					val = val.replace("\"\"", "\"");
				retVal.add(val);
			}
		} else {
			// The line ended without closing a quote, so it's malformed...
			// throw an exception.
			throw new IllegalArgumentException("unclosed quote starting at index: " + (start + 1) + " of: " + line);
		}
		return retVal;
	}

	public static boolean isEmpty(String s) {
		return s == null || s.length() == 0;
	}

	public static boolean isNotEmpty(String s) {
		return s != null && s.length() > 0;
	}

	/**
	 * Converts a const-style String to a variable-style String.
	 * Lowercases the name except for characters following underscores, e.g.,
	 * will change "have_a_nice_day" or "HAVE_A_NICE_DAY" to "haveANiceDay".
	 *
	 * @param constName the String to convert
	 * @return the name if it were a java variable instead of a const
	 */
	public static String constToVariableStyle(String constName) {
		int constNameLen = constName.length();
		StringBuffer variableName = new StringBuffer(constNameLen);
		boolean isUpper = false;
		for (int i = 0; i < constNameLen; i++) {
			char c = constName.charAt(i);
			if (c == '_')
				isUpper = true;
			else if (isUpper) {
				variableName.append(Character.toUpperCase(c));
				isUpper = false;
			} else
				variableName.append(Character.toLowerCase(c));
		}
		return variableName.toString();
	}

	/**
	 * Converts a variable-style String to a const-style String.
	 * Uppercases all characters, and those that are alread upper case will be preceded by an underscore, e.g.,
	 * will change "haveANiceDay" to "HAVE_A_NICE_DAY".
	 *
	 * @param variable the String to convert
	 * @return the name if it were a java variable instead of a const
	 */
	public static String variableToConstStyle(String variable) {
		int variableNameLen = variable.length();
		StringBuffer constName = new StringBuffer(variableNameLen + 4); // most strings shouldn't need more than 4 '_'s
		for (int i = 0; i < variableNameLen; i++) {
			char c = variable.charAt(i);
			if (Character.isUpperCase(c)) {
				constName.append('_');
				constName.append(c);
			} else
				constName.append(Character.toUpperCase(c));
		}
		return constName.toString();
	}

	/**
	 * @param untrimmed the string to trim
	 * @return string with repeated inner whitespace removed and all whitespace off the ends
	 * @noinspection deprecation we need to use isSpace instead of isWhitespace for gwt
	 */
	public static String aggressiveTrim(String untrimmed) {
		if (untrimmed == null)
			return null;

		int length = untrimmed.length();
		StringBuffer trimmed = null;
		int begin = -1, end = -1;

		for (int i = 0; i < length;) {
			char c = untrimmed.charAt(i);

			if (Character.isSpace(c)) {
				if (i++ == end && c == ' ') {
					if (i >= length || Character.isSpace(untrimmed.charAt(i)))
						continue;
				} else
					continue;
			} else if (begin < 0) {
				begin = i; // starting the first section
			} else if (i > end) { // there is more than one section in this String
				trimmed = appendSection(trimmed, untrimmed, begin, end);
				begin = i; // starting the next section
			}
			// continue the existing section
			end = ++i;
		}

		if (trimmed == null) { // there are not multiple sections
			if (begin < 0) // there are no sections, i.e., all whitespace
				return null;
			return untrimmed.substring(begin, end); // there is a single section
		}
		// there are multiple sections, so append the last one and return the whole thing
		return appendSection(trimmed, untrimmed, begin, end).toString();
	}

	public static String getExtension(String filename) {
		int dotIndex = filename.lastIndexOf(EXT_DELIM);
		return dotIndex >= 0 ? filename.substring(dotIndex + 1).toLowerCase() : null;
	}

	public static String join(Collection objects, String delim) {
		if (objects == null)
			return null;
		StringBuffer retVal = new StringBuffer();
		Iterator it = objects.iterator();
		if (it.hasNext()) {
			retVal.append(it.next());
		}
		while (it.hasNext()) {
			retVal.append(delim);
			retVal.append(it.next());
		}
		return retVal.toString();
	}

	public static boolean startsWithNumber(String str) {
		if (isEmpty(str))
			return false;
		char c0 = str.charAt(0);
		return Character.isDigit(c0) || c0 == '-' && str.length() > 1 && Character.isDigit(str.charAt(1));
	}

	public static Long parseLongFromStart(String str) {
		if (isEmpty(str))
			return null;
		int endIndex = str.charAt(0) == '-' ? 1 : 0;
		while (endIndex < str.length() && Character.isDigit(str.charAt(endIndex)))
			endIndex++;
		if (str.charAt(0) == '-' && endIndex == 1)
			return null;
		return Long.parseLong(str.substring(0, endIndex));
	}

	public static boolean emptySafeEquals(String str1, String str2) {
		if (isEmpty(str1))
			str1 = null;
		if (isEmpty(str2))
			str2 = null;
		return ObjectUtils0.nullSafeEquals(str1, str2);
	}

	/**
	 * StringUtils is meant for static method invocations.
	 */
	private StringUtils() {
	}

	private static StringBuffer appendSection(StringBuffer trimmed, String untrimmed, int begin, int end) {
		if (trimmed == null)
			trimmed = new StringBuffer(untrimmed.length()); // trimmed string will have same # of chars or less
		else
			trimmed.append(' '); // this isn't the first section... se separate with a single space

		// It would be nice if there was a StringBuffer.append(String, int, int)...
		// the StringBuffer.append(CharSequence, int, int) doesn't do an arraycopy
		trimmed.append(untrimmed.toCharArray(), begin, end - begin);
		return trimmed;
	}
}
